# coding = utf-8

# 基于物品的协同过滤推荐算法实现
import random

import math
import pymysql                          #数据库
from operator import itemgetter

cnn = pymysql.connect(host='127.0.0.1', user='root', password='123456', port=3306, database='flask_douban_comment',
                      charset='utf8')

class ItemBasedCF():
    # 初始化参数
    def __init__(self):
        # 找到相似的8部电影，为目标用户推荐5部电影
        self.n_sim_movie = 8
        self.n_rec_movie = 5

        # 将数据集划分为训练集和测试集
        self.trainSet = {}
        self.testSet = {}
        # 用户的所有评分记录（用于排除已看过的电影）
        self.user_all_ratings = {}

        # 用户相似度矩阵
        self.movie_sim_matrix = {}
        self.movie_popular = {}
        self.movie_count = 0

        print('Similar movie number = %d' % self.n_sim_movie)
        print('Recommneded movie number = %d' % self.n_rec_movie)

    # 从数据库得到"用户-电影"数据
    def get_dataset(self, pivot=0.75):
        trainSet_len = 0
        testSet_len = 0
        cursor = cnn.cursor()
        sql = ' select * from tb_rate'
        cursor.execute(sql)
        for item in cursor.fetchall():
            user, movie, rating = item[1:]
            
            # 保存用户的所有评分记录
            self.user_all_ratings.setdefault(user, {})
            self.user_all_ratings[user][movie] = rating
            
            # 随机划分训练集和测试集
            if random.random() < pivot:
                self.trainSet.setdefault(user, {})
                self.trainSet[user][movie] = rating
                trainSet_len += 1
            else:
                self.testSet.setdefault(user, {})
                self.testSet[user][movie] = rating
                testSet_len += 1
        cursor.close()

        print('Split trainingSet and testSet success!')
        print('TrainSet = %s' % trainSet_len)
        print('TestSet = %s' % testSet_len)

    # 读文件，返回文件的每一行
    def load_file(self, filename):
        with open(filename, 'r') as f:
            for i, line in enumerate(f):
                if i == 0:  # 去掉文件第一行的title
                    continue
                yield line.strip('\r\n')
        print('Load %s success!' % filename)

    # 计算电影之间的相似度
    def calc_movie_sim(self):
        for user, movies in self.trainSet.items():
            for movie in movies:
                if movie not in self.movie_popular:
                    self.movie_popular[movie] = 0
                self.movie_popular[movie] += 1

        self.movie_count = len(self.movie_popular)
        print("Total movie number = %d" % self.movie_count)

        for user, movies in self.trainSet.items():
            for m1 in movies:
                for m2 in movies:
                    if m1 == m2:
                        continue
                    self.movie_sim_matrix.setdefault(m1, {})
                    self.movie_sim_matrix[m1].setdefault(m2, 0)
                    self.movie_sim_matrix[m1][m2] += 1
        print("Build co-rated users matrix success!")

        # 计算电影之间的相似性 similarity matrix
        print("Calculating movie similarity matrix ...")
        for m1, related_movies in self.movie_sim_matrix.items():
            for m2, count in related_movies.items():
                # 注意0向量的处理，即某电影的用户数为0
                if self.movie_popular[m1] == 0 or self.movie_popular[m2] == 0:
                    self.movie_sim_matrix[m1][m2] = 0
                else:
                    self.movie_sim_matrix[m1][m2] = count / math.sqrt(self.movie_popular[m1] * self.movie_popular[m2])
        print('Calculate movie similarity matrix success!')

    # 针对目标用户U，找到K部相似的电影，并推荐其N部电影
    def recommend(self, user):
        K = self.n_sim_movie
        N = self.n_rec_movie
        rank = {}
        if user > len(self.trainSet):
            user = random.randint(1, len(self.trainSet))
        
        # 使用用户的所有评分记录来判断已看过的电影
        watched_movies = self.user_all_ratings.get(user, {})

        for movie, rating in watched_movies.items():
            if movie not in self.movie_sim_matrix:
                continue
            for related_movie, w in sorted(self.movie_sim_matrix[movie].items(), key=itemgetter(1), reverse=True)[:K]:
                if related_movie in watched_movies:
                    continue
                rank.setdefault(related_movie, 0)
                rank[related_movie] += w * float(rating)
        
        # 按分数排序，取前N*3个候选
        result = sorted(rank.items(), key=itemgetter(1), reverse=True)
        candidates = result[:N * 3]  # 取前N*3个候选
        
        if not candidates:
            return []
        
        # 如果候选数量少于N，直接返回所有候选
        if len(candidates) <= N:
            # 去重，只保留前N个
            seen = set()
            unique_result = []
            for mid, score in candidates:
                if mid not in seen:
                    unique_result.append((mid, score))
                    seen.add(mid)
                if len(unique_result) >= N:
                    break
            return unique_result
        
        # 尝试多样性采样
        try:
            # 获取候选电影的类型和年代信息
            movie_ids = [mid for mid, _ in candidates]
            cursor = cnn.cursor()
            
            # 构建SQL查询，获取电影的类型和年代
            if len(movie_ids) > 0:
                format_strings = ','.join(['%s'] * len(movie_ids))
                sql = f"SELECT id, genres, year FROM movies2 WHERE id IN ({format_strings})"
                cursor.execute(sql, tuple(movie_ids))
                movie_info = {}
                for row in cursor.fetchall():
                    movie_info[row[0]] = {
                        'genres': row[1] if row[1] else '未知',
                        'year': row[2] if row[2] else '2000'
                    }
                cursor.close()
                
                # 按类型分组
                genre_groups = {}
                for mid in movie_ids:
                    if mid in movie_info:
                        genres = movie_info[mid]['genres']
                        # 取第一个类型作为主要类型
                        genre = genres.split('/')[0].strip() if genres and '/' in genres else genres
                        if not genre or genre == '':
                            genre = '未知'
                        genre_groups.setdefault(genre, []).append(mid)
                    else:
                        # 如果数据库中没有找到，归入未知类型
                        genre_groups.setdefault('未知', []).append(mid)
                
                # 多样性采样：每组取1个，优先高分
                selected = []
                for genre, movies in genre_groups.items():
                    if movies:
                        # 从该类型中选分数最高的
                        best_movie = max(movies, key=lambda x: dict(candidates)[x])
                        selected.append(best_movie)
                        if len(selected) >= N:
                            break
                
                # 如果按类型采样后不足N个，用高分电影补齐
                if len(selected) < N:
                    for mid, score in candidates:
                        if mid not in selected:
                            selected.append(mid)
                        if len(selected) >= N:
                            break
                
                # 去重并返回结果
                seen = set()
                unique_result = []
                for mid in selected[:N]:
                    if mid not in seen:
                        score = dict(candidates)[mid]
                        unique_result.append((mid, score))
                        seen.add(mid)
                    if len(unique_result) >= N:
                        break
                
                return unique_result
            else:
                cursor.close()
                # 如果没有候选电影，返回空列表
                return []
                
        except Exception as e:
            print(f"多样性采样出错: {e}，回退到原有推荐逻辑")
            # 出错时回退到原有逻辑
            cursor.close()
        
        # 回退逻辑：直接返回前N个高分电影
        seen = set()
        unique_result = []
        for mid, score in candidates[:N]:
            if mid not in seen:
                unique_result.append((mid, score))
                seen.add(mid)
            if len(unique_result) >= N:
                break
        return unique_result

    # 产生推荐并通过准确率、召回率和覆盖率进行评估
    def evaluate(self):
        print('Evaluating start ...')
        N = self.n_rec_movie
        # 准确率和召回率
        hit = 0
        rec_count = 0
        test_count = 0
        # 覆盖率
        all_rec_movies = set()

        for i, user in enumerate(self.trainSet):
            test_moives = self.testSet.get(user, {})
            rec_movies = self.recommend(user)
            for movie, w in rec_movies:
                if movie in test_moives:
                    hit += 1
                all_rec_movies.add(movie)
            rec_count += N
            test_count += len(test_moives)

        precision = hit / (1.0 * rec_count)
        recall = hit / (1.0 * test_count)
        coverage = len(all_rec_movies) / (1.0 * self.movie_count)
        print('precisioin=%.4f\trecall=%.4f\tcoverage=%.4f' % (precision, recall, coverage))

    def rec_one(self,userId):
        print('推荐一个')
        rec_movies = self.recommend(userId)
        # print(rec_movies)
        return rec_movies

# itemCF 推荐算法接口
def recommend(userId, top_n=5):
    itemCF = ItemBasedCF()
    itemCF.get_dataset()
    itemCF.calc_movie_sim()
    # 设置推荐数量
    itemCF.n_rec_movie = top_n
    recs = itemCF.rec_one(userId)
    return recs